package conve

import (
	"errors"
	"fmt"
	"github.com/gogf/gf/text/gstr"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"os"
	"os/exec"
	"path"
	"strings"
)

//map for converting mysql type to golang types
var typeForMysqlToGo = map[string]string{
	"int":                "int",
	"integer":            "int",
	"tinyint":            "int",
	"smallint":           "int",
	"mediumint":          "int",
	"bigint":             "int",
	"int unsigned":       "int",
	"integer unsigned":   "int",
	"tinyint unsigned":   "int",
	"smallint unsigned":  "int",
	"mediumint unsigned": "int",
	"bigint unsigned":    "int",
	"bit":                "int",
	"bool":               "bool",
	"enum":               "string",
	"set":                "string",
	"varchar":            "string",
	"char":               "string",
	"tinytext":           "string",
	"mediumtext":         "string",
	"text":               "string",
	"longtext":           "string",
	"blob":               "string",
	"tinyblob":           "string",
	"mediumblob":         "string",
	"longblob":           "string",
	"date":               "time.Time", // time.Time
	"datetime":           "time.Time", // time.Time
	"timestamp":          "time.Time", // time.Time
	"time":               "time.Time", // time.Time
	"float":              "float64",
	"double":             "float64",
	"decimal":            "float64",
	"binary":             "string",
	"varbinary":          "string",
}

type Table2Struct struct {
	dsn            string
	savePath       string
	db             *gorm.DB
	table          string
	prefix         string
	config         *T2tConfig
	err            error
	realNameMethod string
	enableJsonTag  bool   // 是否添加json的tag, 默认不添加
	packageName    string // 生成struct的包名(默认为空的话, 则取名为: package model)
	tagKey         string // tag字段的key值,默认是orm
}

type T2tConfig struct {
	RmTagIfUcFirsted bool // 如果字段首字母本来就是大写, 就不添加tag, 默认false添加, true不添加
	TagToLower       bool // tag的字段名字是否转换为小写, 如果本身有大写字母的话, 默认false不转
	UcFirstOnly      bool // 字段首字母大写的同时, 是否要把其他字母转换为小写,默认false不转换
	SeperatFile      bool // 每个struct放入单独的文件,默认false,放入同一个文件
}

func NewTable2Struct() *Table2Struct {
	return &Table2Struct{}
}

func (t *Table2Struct) Dsn(d string) *Table2Struct {
	t.dsn = d
	return t
}

func (t *Table2Struct) TagKey(r string) *Table2Struct {
	t.tagKey = r
	return t
}

func (t *Table2Struct) PackageName(r string) *Table2Struct {
	t.packageName = r
	return t
}

func (t *Table2Struct) RealNameMethod(r string) *Table2Struct {
	t.realNameMethod = r
	return t
}

func (t *Table2Struct) SavePath(p string) *Table2Struct {
	t.savePath = p
	return t
}

func (t *Table2Struct) DB(d *gorm.DB) *Table2Struct {
	t.db = d
	return t
}

func (t *Table2Struct) Table(tab string) *Table2Struct {
	t.table = tab
	return t
}

func (t *Table2Struct) Prefix(p string) *Table2Struct {
	t.prefix = p
	return t
}

func (t *Table2Struct) EnableJsonTag(p bool) *Table2Struct {
	t.enableJsonTag = p
	return t
}

func (t *Table2Struct) Config(c *T2tConfig) *Table2Struct {
	t.config = c
	return t
}

func (t *Table2Struct) Run() error {
	if t.config == nil {
		t.config = new(T2tConfig)
	}
	// 链接mysql, 获取db对象
	t.dialMysql()
	if t.err != nil {
		return t.err
	}

	// 获取表和字段的shcema
	tableColumns, err := t.getColumns()
	if err != nil {
		return err
	}

	// 包名
	var packageName string
	var clsPackageName = "package cls\n\n"

	if t.packageName == "" {
		packageName = "package model\n\n"
	} else {
		packageName = fmt.Sprintf("package %s\n\n", t.packageName)
	}

	// 组装struct
	for tableRealName, item := range tableColumns {
		var structContent string
		var clsContent string
		var clsCon string
		// 去除前缀
		if t.prefix != "" {
			tableRealName = tableRealName[len(t.prefix):]
		}
		depth := 1
		tableStructName := gstr.CaseCamel(tableRealName)
		structContent += "type " + tableStructName + " struct {\n"
		clsContent += "type cls" + tableStructName + " struct {\n"

		clsCon += "var Cls" + tableStructName + " = &cls" + tableStructName + "{\n"

		for _, v := range item {
			// 字段注释
			var clumnComment string
			if v.ColumnComment != "" {
				clumnComment = fmt.Sprintf(" // %s", v.ColumnComment)
			}
			structContent += fmt.Sprintf("%s%s %s %s%s\n",
				tab(depth), v.ColumnName, v.Type, v.Tag, clumnComment)

			clsContent += fmt.Sprintf("%s%s string \n", tab(depth), v.ColumnName)
			clsCon += fmt.Sprintf("%s %s: \"%s\", \n", tab(depth), v.ColumnName, v.RealColName)
		}
		structContent += tab(depth-1) + "}\n\n"
		clsContent += tab(depth-1) + "}\n\n"
		clsCon += tab(depth-1) + "}\n\n"

		// 添加 method 获取真实表名
		if t.realNameMethod != "" {
			structContent += fmt.Sprintf("func (*%s) %s() string {\n",
				tableStructName, t.realNameMethod)
			structContent += fmt.Sprintf("%sreturn \"%s\"\n",
				tab(depth), tableRealName)
			structContent += "}\n\n"
		}
		//fmt.Println(structContent)
		// 如果有引入 time.Time, 则需要引入 time 包
		var importContent string
		if strings.Contains(structContent, "time.Time") {
			importContent = "import \"time\"\n\n"
		}

		// 写入文件struct
		var savePath = t.savePath
		if !pathExists(savePath) {
			_ = os.Mkdir(savePath, 0777)
		}
		clsPath := path.Join(savePath, "/cls")
		if !pathExists(clsPath) {
			_ = os.Mkdir(clsPath, 0777)
		}
		filePath := path.Join(savePath, tableRealName+".go")
		f, err := os.Create(filePath)
		if err != nil {
			fmt.Println("Can not write file")
			return err
		}
		_, _ = f.WriteString(packageName + importContent + structContent)
		clsFilePath := path.Join(clsPath, tableRealName+".go")
		f1, err := os.Create(clsFilePath)
		if err != nil {
			fmt.Println("Can not write file")
			return err
		}
		_, _ = f1.WriteString(clsPackageName + clsContent + clsCon)

		_ = f1.Close()
		_ = f.Close()
		exec.Command("gofmt", "-w", filePath).Run()
		exec.Command("gofmt", "-w", clsFilePath).Run()
		fmt.Println(tableRealName + ".go " + "生成完成")
	}

	return nil
}
func pathExists(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	return false
}

func (t *Table2Struct) dialMysql() {
	if t.db == nil {
		if t.dsn == "" {
			t.err = errors.New("dsn数据库配置缺失")
			return
		}
		t.db, t.err = gorm.Open(mysql.Open(t.dsn))
	}
	return
}

type column struct {
	ColumnName    string `gorm:"column:column_name"`
	RealColName   string `gorm:"column:real_col_name"`
	Type          string `gorm:"column:type"`
	Nullable      string `gorm:"column:nullable"`
	TableName     string `gorm:"column:table_name"`
	ColumnComment string `gorm:"column:column_comment"`
	Tag           string `gorm:"column:tag"`
}

// Function for fetching schema definition of passed table
func (t *Table2Struct) getColumns() (tableColumns map[string][]column, err error) {
	tableColumns = make(map[string][]column)
	// sql
	var sqlStr = `SELECT column_name,column_name as real_col_name,data_type as type,is_nullable as nullable,table_name,column_comment
		FROM information_schema.COLUMNS 
		WHERE table_schema = DATABASE()`
	// 是否指定了具体的table
	bind := make([]interface{}, 0)
	if len(t.table) > 0 {
		sqlStr += fmt.Sprintf(" AND TABLE_NAME in (?)")
		bind = append(bind, strings.Split(t.table, ","))
	}

	// sql排序
	sqlStr += " order by TABLE_NAME asc, ORDINAL_POSITION asc"
	var rows []column
	err = t.db.Raw(sqlStr, bind...).Find(&rows).Error
	if err != nil {
		fmt.Println("Error reading table information: ", err.Error())
		return
	}

	for _, col := range rows {
		col.Tag = col.ColumnName
		col.RealColName = col.ColumnName
		replaceMap := map[string]string{
			"id":  "ID",
			"url": "URL",
		}
		for k, v := range replaceMap {
			col.ColumnName = strings.ReplaceAll(col.ColumnName, k, v)
		}
		col.ColumnName = t.camelCase(col.ColumnName)
		col.Type = typeForMysqlToGo[col.Type]
		if col.Type == "time.Time" && col.Nullable == "YES" {
			col.Type = "*time.Time"
		}

		// 字段首字母本身大写, 是否需要删除tag
		if t.config.RmTagIfUcFirsted &&
			col.ColumnName[0:1] == strings.ToUpper(col.ColumnName[0:1]) {
			col.Tag = "-"
		} else {
			// 是否需要将tag转换成小写
			if t.config.TagToLower {
				col.Tag = strings.ToLower(col.Tag)
			}
		}
		if t.enableJsonTag {
			col.Tag = fmt.Sprintf("`%s:\"column:%s\" json:\"%s\"`", t.tagKey, col.Tag, col.Tag)
		} else {
			col.Tag = fmt.Sprintf("`%s:\"column:%s\"`", t.tagKey, col.Tag)
		}
		if _, ok := tableColumns[col.TableName]; !ok {
			tableColumns[col.TableName] = []column{}
		}
		tableColumns[col.TableName] = append(tableColumns[col.TableName], col)
	}

	return
}

func (t *Table2Struct) camelCase(str string) string {
	// 是否有表前缀, 设置了就先去除表前缀
	if t.prefix != "" {
		str = strings.Replace(str, t.prefix, "", 1)
	}
	var text string
	//for _, p := range strings.Split(name, "_") {
	for _, p := range strings.Split(str, "_") {
		// 字段首字母大写的同时, 是否要把其他字母转换为小写
		switch len(p) {
		case 0:
		case 1:
			text += strings.ToUpper(p[0:1])
		default:
			// 字符长度大于1时
			if t.config.UcFirstOnly == true {
				text += strings.ToUpper(p[0:1]) + strings.ToLower(p[1:])
			} else {
				text += strings.ToUpper(p[0:1]) + p[1:]
			}
		}
	}
	return text
}
func tab(depth int) string {
	return strings.Repeat("\t", depth)
}
